package au.com.finalconcept.roo.addon.behaviours;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Logger;

import org.apache.commons.lang3.Validate;
import org.springframework.roo.classpath.TypeLocationService;
import org.springframework.roo.classpath.TypeManagementService;
import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails;
import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder;
import org.springframework.roo.classpath.details.FieldMetadata;
import org.springframework.roo.classpath.details.FieldMetadataBuilder;
import org.springframework.roo.classpath.details.MemberFindingUtils;
import org.springframework.roo.classpath.details.MethodMetadata;
import org.springframework.roo.classpath.details.MethodMetadataBuilder;
import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType;
import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue;
import org.springframework.roo.classpath.details.annotations.AnnotationMetadata;
import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder;
import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder;
import org.springframework.roo.model.JavaSymbolName;
import org.springframework.roo.model.JavaType;

public class Util {

    public static final String METHOD_NAME_PRE_UPDATE_IMPL = "preUpdateImpl";
    public static final String METHOD_NAME_PRE_PERSIST_IMPL = "prePersistImpl";

    public static FieldMetadata getDateField(String id, String fieldNameString, String dbColumnNameString) {
        // Using the FieldMetadataBuilder to create the field definition.
        FieldMetadataBuilder fieldBuilder;
        AnnotationMetadataBuilder annotationBuilder;
        ArrayList<AnnotationMetadataBuilder> annotationBuilders;
        JavaSymbolName fieldName;
        JavaType fieldType;

        annotationBuilders = new ArrayList<AnnotationMetadataBuilder>();
        fieldName = new JavaSymbolName(fieldNameString);
        fieldType = new JavaType(Date.class);

        // @javax.persistence.Temporal(value =
        // javax.persistence.TemporalType.TIMESTAMP)
        annotationBuilder = new AnnotationMetadataBuilder(new JavaType("javax.persistence.Temporal"));
        annotationBuilder.addEnumAttribute("value", "javax.persistence.TemporalType", "TIMESTAMP");
        annotationBuilders.add(annotationBuilder);

        // @javax.persistence.Column(name = "deleted_at", nullable = true)
        annotationBuilder = new AnnotationMetadataBuilder("javax.persistence.Column");
        annotationBuilder.addStringAttribute("name", dbColumnNameString);
        annotationBuilder.addBooleanAttribute("nullable", true);
        annotationBuilders.add(annotationBuilder);

        // @DateTimeFormat(style = "MM")
        annotationBuilder = new AnnotationMetadataBuilder("org.springframework.format.annotation.DateTimeFormat");
        annotationBuilder.addStringAttribute("style", "MM");
        annotationBuilders.add(annotationBuilder);

        fieldBuilder = new FieldMetadataBuilder(id, Modifier.PRIVATE, annotationBuilders, fieldName, fieldType);

        return fieldBuilder.build(); // Build and return a FieldMetadata
                                     // instance
    }

    // Typically, no changes are required beyond this point

    public static FieldMetadata getStringField(String id, String fieldNameString, String dbColumnNameString, int sizeMin, int sizeMax,
            boolean allownNull) {
        // Using the FieldMetadataBuilder to create the field definition.
        FieldMetadataBuilder fieldBuilder;
        AnnotationMetadataBuilder annotationBuilder;
        ArrayList<AnnotationMetadataBuilder> annotationBuilders;
        JavaSymbolName fieldName;
        JavaType fieldType;

        annotationBuilders = new ArrayList<AnnotationMetadataBuilder>();
        fieldName = new JavaSymbolName(fieldNameString);
        fieldType = new JavaType(String.class);

        // @javax.persistence.Temporal(value =
        // javax.persistence.TemporalType.TIMESTAMP)
        // @javax.persistence.Column(name = "deleted_at", nullable = true)
        annotationBuilder = new AnnotationMetadataBuilder("javax.persistence.Column");
        annotationBuilder.addStringAttribute("name", dbColumnNameString);
        annotationBuilder.addBooleanAttribute("nullable", allownNull);
        annotationBuilders.add(annotationBuilder);
        annotationBuilder = new AnnotationMetadataBuilder(new JavaType("javax.validation.constraints.Size"));
        annotationBuilder.addIntegerAttribute("min", sizeMin);
        annotationBuilder.addIntegerAttribute("max", sizeMax);

        fieldBuilder = new FieldMetadataBuilder(id, Modifier.PRIVATE, annotationBuilders, fieldName, fieldType);

        return fieldBuilder.build(); // Build and return a FieldMetadata
                                     // instance
    }

    // Typically, no changes are required beyond this point
    public static MethodMetadata getGetterMethod(ClassOrInterfaceTypeDetails governorTypeDetails, Class<?> returnType, String id,
            String methodNameString, String memberNameString) {

        final JavaSymbolName methodName = new JavaSymbolName(methodNameString);

        MethodMetadata method = Util.methodExists(governorTypeDetails, methodName, new ArrayList<AnnotatedJavaType>());
        if (method != null) {
            // If it already exists, just return the method and omit its
            // generation via the ITD
            return method;
        }

        // Define method annotations (none in this case)
        List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();

        // Define method throws types (none in this case)
        List<JavaType> throwsTypes = new ArrayList<JavaType>();

        // Define method parameter types (none in this case)
        List<AnnotatedJavaType> parameterTypes = new ArrayList<AnnotatedJavaType>();

        // Define method parameter names (none in this case)
        List<JavaSymbolName> parameterNames = new ArrayList<JavaSymbolName>();

        // Create the method body
        InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
        bodyBuilder.appendFormalLine("return this." + memberNameString + ";");

        // Use the MethodMetadataBuilder for easy creation of MethodMetadata
        MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder(
                id,
                    Modifier.PUBLIC,
                    methodName,
                    new JavaType(returnType),
                    parameterTypes,
                    parameterNames,
                    bodyBuilder);
        methodBuilder.setAnnotations(annotations);
        methodBuilder.setThrowsTypes(throwsTypes);

        return methodBuilder.build(); // Build and return a MethodMetadata
                                      // instance
    }

    /**
     * Creates the getter for the deletedAt field
     * 
     * @return
     */
    public static MethodMetadata getSetterMethod(ClassOrInterfaceTypeDetails governorTypeDetails, Class<?> valueType, String id,
            String methodNameString, String memberName) {

        final JavaSymbolName methodName = new JavaSymbolName(methodNameString);

        MethodMetadata method = Util.methodExists(governorTypeDetails, methodName, new ArrayList<AnnotatedJavaType>());
        if (method != null) {
            // If it already exists, just return the method and omit its
            // generation via the ITD
            return method;
        }

        // Define method annotations (none in this case)
        ArrayList<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();

        // Define method throws types (none in this case)
        List<JavaType> throwsTypes = new ArrayList<JavaType>();

        // Define method parameter types
        List<AnnotatedJavaType> parameterTypes = new ArrayList<AnnotatedJavaType>();
        List<AnnotationMetadata> annotationMetadataList;
        AnnotatedJavaType type;
        annotationMetadataList = new ArrayList<AnnotationMetadata>();
        type = new AnnotatedJavaType(new JavaType(valueType), annotationMetadataList);
        parameterTypes.add(type);

        // Define method parameter names (none in this case)
        List<JavaSymbolName> parameterNames = new ArrayList<JavaSymbolName>();
        parameterNames.add(new JavaSymbolName("value"));

        // Create the method body
        InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
        bodyBuilder.appendFormalLine("this." + memberName + " = value;");

        // Use the MethodMetadataBuilder for easy creation of MethodMetadata
        MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder(
                id,
                    Modifier.PUBLIC,
                    methodName,
                    JavaType.VOID_PRIMITIVE,
                    parameterTypes,
                    parameterNames,
                    bodyBuilder);
        methodBuilder.setAnnotations(annotations);
        methodBuilder.setThrowsTypes(throwsTypes);

        return methodBuilder.build(); // Build and return a MethodMetadata
                                      // instance
    }

    // Typically, no changes are required beyond this point

    public static MethodMetadata methodExists(ClassOrInterfaceTypeDetails governorTypeDetails, JavaSymbolName methodName,
            List<AnnotatedJavaType> paramTypes) {
        // We have no access to method parameter information, so we scan by name
        // alone and treat any match as authoritative
        // We do not scan the superclass, as the caller is expected to know
        // we'll only scan the current class
        for (MethodMetadata method : governorTypeDetails.getDeclaredMethods()) {
            if (method.getMethodName().equals(methodName) && method.getParameterTypes().equals(paramTypes)) {
                // Found a method of the expected name; we won't check method
                // parameters though
                return method;
            }
        }
        return null;
    }
/*
    public static MethodMetadata getPrePersistImplMethod(ClassOrInterfaceTypeDetails governorTypeDetails, String id, Logger log) {
        final JavaSymbolName methodName = new JavaSymbolName(METHOD_NAME_PRE_PERSIST_IMPL);
        List<AnnotatedJavaType> parameterTypes;
        List<JavaSymbolName> parameterNames;
        InvocableMemberBodyBuilder bodyBuilder;
        ArrayList<AnnotationMetadataBuilder> annotations;
        List<JavaType> throwsTypes;

        MethodMetadata method = Util.methodExists(governorTypeDetails, methodName, new ArrayList<AnnotatedJavaType>());
        if (method != null) {
            log.info("Found method of type preUpdateImp on entity " + governorTypeDetails.getName() + " will not create in the ITD");
            return method;
        }

        // Define method annotations (PrePersist in this case)
        // Define method throws types (none in this case)
        annotations = new ArrayList<AnnotationMetadataBuilder>();
        throwsTypes = new ArrayList<JavaType>();
        parameterTypes = new ArrayList<AnnotatedJavaType>();
        parameterNames = new ArrayList<JavaSymbolName>();

        // Create the method body
        bodyBuilder = new InvocableMemberBodyBuilder();
        bodyBuilder.appendFormalLine("// To be implemented by the user");

        // Use the MethodMetadataBuilder for easy creation of MethodMetadata
        MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder(
                id,
                    Modifier.PUBLIC,
                    methodName,
                    JavaType.VOID_PRIMITIVE,
                    parameterTypes,
                    parameterNames,
                    bodyBuilder);
        methodBuilder.setAnnotations(annotations);
        methodBuilder.setThrowsTypes(throwsTypes);

        return methodBuilder.build(); // Build and return a MethodMetadata
                                      // instance
    }

    public static MethodMetadata getPreUpdateImplMethod(ClassOrInterfaceTypeDetails governorTypeDetails, String id, Logger log) {
        final JavaSymbolName methodName = new JavaSymbolName(METHOD_NAME_PRE_UPDATE_IMPL);
        List<AnnotatedJavaType> parameterTypes;
        List<JavaSymbolName> parameterNames;
        InvocableMemberBodyBuilder bodyBuilder;

        MethodMetadata method = Util.methodExists(governorTypeDetails, methodName, new ArrayList<AnnotatedJavaType>());
        if (method != null) {
            log.info("Found method of type prePersistImpl on entity " + governorTypeDetails.getName() + " will not create in the ITD");
            return method;
        }

        // Define method annotations (PrePersist in this case)
        ArrayList<AnnotationMetadataBuilder> annotations;

        // Define method throws types (none in this case)
        List<JavaType> throwsTypes;

        annotations = new ArrayList<AnnotationMetadataBuilder>();
        throwsTypes = new ArrayList<JavaType>();
        parameterTypes = new ArrayList<AnnotatedJavaType>();
        parameterNames = new ArrayList<JavaSymbolName>();

        // Create the method body
        bodyBuilder = new InvocableMemberBodyBuilder();
        bodyBuilder.appendFormalLine("// To be implemented by the user");

        // Use the MethodMetadataBuilder for easy creation of MethodMetadata
        MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder(
                id,
                    Modifier.PUBLIC,
                    methodName,
                    JavaType.VOID_PRIMITIVE,
                    parameterTypes,
                    parameterNames,
                    bodyBuilder);
        methodBuilder.setAnnotations(annotations);
        methodBuilder.setThrowsTypes(throwsTypes);

        return methodBuilder.build(); // Build and return a MethodMetadata
                                      // instance
    }
*/
    /**
     * 
     * @param typeLocationService
     * @param typeManagementService
     * @param javaType
     * @param annotationType
     * @param attributes A key/value set of STRING attributes to add to the attirbute
     * @param addOnPrePersistInterface
     */
    public static void annotateIfNeeded(TypeLocationService typeLocationService, TypeManagementService typeManagementService,
            JavaType javaType, JavaType annotationType, HashMap<String, String> attributes) {

        // Use Roo's Assert type for null checks
        Validate.notNull(javaType, "Java type required");

        // Obtain ClassOrInterfaceTypeDetails for this java type
        ClassOrInterfaceTypeDetails existing;
        AnnotationMetadata annotation;

        existing = typeLocationService.getTypeDetails(javaType);

        // Test if the annotation already exists on the target type
        annotation = MemberFindingUtils.getAnnotationOfType(existing.getAnnotations(), annotationType);

        if (existing != null && annotation == null) {
            AnnotationMetadataBuilder annotationBuilder;
            ClassOrInterfaceTypeDetailsBuilder classOrInterfaceTypeDetailsBuilder;

            classOrInterfaceTypeDetailsBuilder = new ClassOrInterfaceTypeDetailsBuilder(existing);
            // Create Annotation metadata
            annotationBuilder = new AnnotationMetadataBuilder(annotationType);

            if(attributes != null && attributes.size() > 0){
                Iterator<String> keys;
                
                keys = attributes.keySet().iterator();
                while(keys.hasNext()){
                    String key;
                    
                    key = keys.next();
                    
                    annotationBuilder.addStringAttribute(key, attributes.get(key));
                }
            }
            // Add annotation to target type
            classOrInterfaceTypeDetailsBuilder.addAnnotation(annotationBuilder.build());

            // Save changes to disk
            typeManagementService.createOrUpdateTypeOnDisk(classOrInterfaceTypeDetailsBuilder.build());
        }

    }
}
